Skip to content

fix(expo): initialize Android native components without event methods#8920

Merged
wobsoriano merged 4 commits into
mainfrom
mike/fix-expo-android-native-bootstrap
Jun 18, 2026
Merged

fix(expo): initialize Android native components without event methods#8920
wobsoriano merged 4 commits into
mainfrom
mike/fix-expo-android-native-bootstrap

Conversation

@mikepitre

@mikepitre mikepitre commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fix Android native component initialization by validating the actual Clerk bootstrap contract instead of assuming every Expo module also exposes React Native event-emitter bookkeeping methods.
  • On Android, the Expo module object used for native bootstrap can provide configure/getClientToken/syncFromJsClientToken without addListener/removeListeners. The previous guard treated those event methods as required, rejected the module, and skipped configure, leaving AuthView mounted against an uninitialized native SDK.
  • Keep native client event subscription separate from bootstrap validation, so modules without an event-emitter contract are not called as emitters.
  • Keep the injected iOS bridge internal to the app target while using the same plain ClerkExpo import style as Expo's generated files.

Repro

On the native-components quickstart using the published 3.4.6 package, Android can render the AuthView shell without fields because the bootstrap code rejects the native module before calling configure. Packing this branch into that app restores the expected AuthView fields and social buttons.

Validation

Use the native-components quickstart with a package tarball from this branch, then run clean native prebuilds for both platforms. iOS and Android both build/run, and Android AuthView renders the expected fields/buttons.

Summary by CodeRabbit

  • Bug Fixes
    • Fixed Android native component initialization and auth syncing when React Native event listener APIs are unavailable.
    • Improved native module/event handling to support modules that omit listener management methods.
    • Fixed Android client-token hydration and sync behavior after JS token updates.
  • Chores
    • Updated the iOS native bridge for improved Swift compatibility and import visibility handling.

@changeset-bot

changeset-bot Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 57c1d23

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@clerk/expo Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-js-sandbox Ready Ready Preview, Comment Jun 18, 2026 10:33pm
swingset Ready Ready Preview, Comment Jun 18, 2026 10:33pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

An error occurred during the review process. Please try again later.

📝 Walkthrough

Walkthrough

The PR refactors Clerk Expo's native module initialization and readiness synchronization. Event listener methods (addListener, removeListeners) are made optional in the native module type contract and removed from required checks. The JS hook introduces a getNativeClientEventEmitter() helper to gracefully handle modules without listener methods, exiting early on Android when unavailable. Both Android and iOS native layers switch readiness waits from sessionFlow/session-based polling to clientFlow/Clerk.isLoaded-based polling. iOS ClerkNativeBridge reduces its public API surface by removing public modifiers from the class and all entry-point methods. Android's syncFromJsClientToken optimizes by short-circuiting when the incoming token already matches the current device token. Two changesets document the fixes.

Changes

Expo Native Module Readiness and Event Listener Refactor

Layer / File(s) Summary
Optional event listeners in native module type and guard
packages/expo/src/utils/native-module.ts, packages/expo/src/utils/__tests__/native-module.test.ts
addListener and removeListeners marked optional on ClerkExpoNativeModule. isClerkExpoModule type guard now requires only configure, getClientToken, and syncFromJsClientToken. Test helper accepts includeEventMethods option; test cases validate bootstrap contract with and without event methods.
Event emitter hook: graceful handling of optional listeners
packages/expo/src/hooks/useNativeClientEvents.ts, packages/expo/src/hooks/__tests__/useNativeClientEvents.test.ts
getNativeClientEventEmitter() helper selects emitter based on platform (iOS → DeviceEventEmitter; Android → ClerkExpo.addListener if exists, else null). useEffect exits early when no emitter available. Test mocks refactored into shared object; new test confirms Android gracefully skips subscription without listeners and emits no console errors.
Android native readiness: session-to-client-token synchronization
packages/expo/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt
Switches readiness waits from Clerk.sessionFlow to Clerk.clientFlow across initial bootstrap, reconfiguration, and token-update paths. syncFromJsClientToken short-circuits when current device token already matches incoming token, skipping update/refresh flows. Unused Client import removed; timeout debug logs updated to reference client instead of session.
iOS bridge: session-to-client-token sync and visibility tightening
packages/expo/ios/ClerkNativeBridge.swift
ClerkNativeBridge class and all public methods (register(), configure(), getClientToken(), makeAuthViewController(), makeUserProfileViewController(), makeUserButtonViewController(), syncFromJsClientToken()) lose public modifiers (now internal). Readiness polling refactored from session-based (waitForLoadedSession, requires session and user non-nil) to client-based (waitForLoadedClient, requires Clerk.shared.isLoaded). Configuration and token-sync paths updated to use client-based waits.
Release changesets
.changeset/great-rivers-pull.md, .changeset/android-native-client-sync.md
Two changesets declare @clerk/expo patch releases: one documents iOS Swift bridge visibility fix; the other documents Android native auth state sync optimization when JS layer syncs client token back to native SDK.

Sequence Diagram(s)

sequenceDiagram
  participant App
  participant useNativeClientEvents
  participant getNativeClientEventEmitter
  participant Platform
  participant ClerkExpo
  participant DeviceEventEmitter
  participant RNEventEmitter as Native Event Handler

  App->>useNativeClientEvents: Render with native client
  useNativeClientEvents->>getNativeClientEventEmitter: Call in useEffect
  getNativeClientEventEmitter->>Platform: Check Platform.OS

  alt iOS Platform
    Platform-->>getNativeClientEventEmitter: 'ios'
    getNativeClientEventEmitter-->>useNativeClientEvents: Return DeviceEventEmitter
    useNativeClientEvents->>DeviceEventEmitter: subscribe('clerkNativeClientChanged')
    RNEventEmitter-->>useNativeClientEvents: Fire event on client change
  else Android with addListener
    Platform-->>getNativeClientEventEmitter: 'android'
    getNativeClientEventEmitter->>ClerkExpo: Check addListener exists
    ClerkExpo-->>getNativeClientEventEmitter: ✓ addListener exists
    getNativeClientEventEmitter-->>useNativeClientEvents: Return ClerkExpo
    useNativeClientEvents->>ClerkExpo: addListener('clerkNativeClientChanged')
    RNEventEmitter-->>useNativeClientEvents: Fire event on client change
  else Android without addListener
    Platform-->>getNativeClientEventEmitter: 'android'
    getNativeClientEventEmitter->>ClerkExpo: Check addListener exists
    ClerkExpo-->>getNativeClientEventEmitter: ✗ addListener undefined
    getNativeClientEventEmitter-->>useNativeClientEvents: Return null
    useNativeClientEvents->>useNativeClientEvents: Early return (no subscription)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • clerk/javascript#8879: Introduces the new clerkNativeClientChanged event and syncFromJsClientToken bidirectional sync model that this PR's readiness refactoring and short-circuit optimization directly support.

Suggested reviewers

  • wobsoriano

Poem

🐇 A rabbit hops through native code,
Making listeners optional (the lighter load),
Session waits transform to client polls,
Both Android and iOS achieve their goals,
Visibility shrinks, short-circuits bloom—
A patch so clean, it clears the room! 🌿

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing Android native component initialization when event methods are absent, which is the core issue addressed across all files in this changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@pkg-pr-new

pkg-pr-new Bot commented Jun 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

@clerk/astro

npm i https://pkg.pr.new/@clerk/astro@8920

@clerk/backend

npm i https://pkg.pr.new/@clerk/backend@8920

@clerk/chrome-extension

npm i https://pkg.pr.new/@clerk/chrome-extension@8920

@clerk/clerk-js

npm i https://pkg.pr.new/@clerk/clerk-js@8920

@clerk/eslint-plugin

npm i https://pkg.pr.new/@clerk/eslint-plugin@8920

@clerk/expo

npm i https://pkg.pr.new/@clerk/expo@8920

@clerk/expo-passkeys

npm i https://pkg.pr.new/@clerk/expo-passkeys@8920

@clerk/express

npm i https://pkg.pr.new/@clerk/express@8920

@clerk/fastify

npm i https://pkg.pr.new/@clerk/fastify@8920

@clerk/hono

npm i https://pkg.pr.new/@clerk/hono@8920

@clerk/localizations

npm i https://pkg.pr.new/@clerk/localizations@8920

@clerk/nextjs

npm i https://pkg.pr.new/@clerk/nextjs@8920

@clerk/nuxt

npm i https://pkg.pr.new/@clerk/nuxt@8920

@clerk/react

npm i https://pkg.pr.new/@clerk/react@8920

@clerk/react-router

npm i https://pkg.pr.new/@clerk/react-router@8920

@clerk/shared

npm i https://pkg.pr.new/@clerk/shared@8920

@clerk/tanstack-react-start

npm i https://pkg.pr.new/@clerk/tanstack-react-start@8920

@clerk/testing

npm i https://pkg.pr.new/@clerk/testing@8920

@clerk/ui

npm i https://pkg.pr.new/@clerk/ui@8920

@clerk/upgrade

npm i https://pkg.pr.new/@clerk/upgrade@8920

@clerk/vue

npm i https://pkg.pr.new/@clerk/vue@8920

commit: 57c1d23

@mikepitre mikepitre requested a review from wobsoriano June 18, 2026 20:18
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

API Changes Report

Generated by Break Check on 2026-06-18T22:35:03.923Z

Summary

Metric Count
Packages analyzed 19
Packages with changes 0
🔴 Breaking changes 0
🟡 Non-breaking changes 0
🟢 Additions 0

No API Changes Detected

All packages have stable APIs with no detected changes.


Report generated by Break Check

Last ran on 57c1d23.

@mikepitre mikepitre marked this pull request as draft June 18, 2026 20:23

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 57c1d23b83

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".


const sourceId = `${nativeClientSyncSourceIdPrefix}-${generation}`;
await ClerkExpo.syncFromJsClientToken(bearerToken, sourceId);
await ClerkExpo.syncFromJsClientToken(bearerToken, sourceId, options.shouldRefreshClient);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the two-argument native sync call

Passing shouldRefreshClient unconditionally changes the JS/native bridge arity for syncFromJsClientToken. Apps can receive this JS through an OTA/update while still running the native module from the previous release, whose iOS and Android exports only accept (clientToken, sourceId); in that mixed-version case the bridge rejects or misroutes the promise callbacks before native sync runs, so token-cache changes stop syncing. Keep the existing two-argument call for cases with old semantics, or gate a new native method/capability before sending the third argument.

Useful? React with 👍 / 👎.

return ClerkExpo as RefreshClientEventEmitter;
}

return null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Route Android native-client events without addListener

For the Android module shape this patch now accepts for bootstrap (configure/getClientToken/syncFromJsClientToken but no addListener), this returns null, so useNativeClientEvents never subscribes to clerkNativeClientChanged. The Android module still emits that event from Clerk.clientFlow, and NativeClientSync relies on it to reload JS state/token cache after native components change auth state; with no listener, native-driven sign-ins or session changes leave JS signed out/stale. Use a fallback emitter or otherwise handle native-to-JS sync for the no-addListener path.

Useful? React with 👍 / 👎.

@wobsoriano wobsoriano left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good 👍🏼

Tested locally in ios and android simulators

@wobsoriano wobsoriano merged commit e4f7d8d into main Jun 18, 2026
47 checks passed
@wobsoriano wobsoriano deleted the mike/fix-expo-android-native-bootstrap branch June 18, 2026 22:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants